spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { EventDTO, EventType } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { notFound } from "next/navigation";5import { Breadcrumbs } from "@/components/entity/breadcrumbs";6import { EntityMap } from "@/components/entity/entity-map";7import { JsonLd } from "@/components/entity/json-ld";8import { ExtLink } from "@/components/entity/kv";9import { fPageHref, PagedFacilities, parseFPage } from "@/components/entity/paged-facilities";10import { SectionNav } from "@/components/entity/section-nav";11import { SourcesFooter } from "@/components/entity/sources-footer";12import { EventList } from "@/components/event-list";13import { ClaimsTable, DataQualityPanel } from "@/components/evidence";14import { FacilityTable } from "@/components/facility-table";15import { ExpansionVelocityBlock } from "@/components/intelligence/operator/velocity";16import { CoverageBar, HonestEmpty, MwFigure, PillLink, PipelineBars, PublishedOnlyNote, ShareList } from "@/components/intelligence/shared";17import { WatchButton } from "@/components/intelligence/watch-button";18import { ProjectList } from "@/components/project-list";19import { Badge } from "@/components/ui/badge";20import { Page, Section } from "@/components/ui/card";21import { StackedBar } from "@/components/ui/charts";22import { CountryFlag } from "@/components/ui/country-flag";23import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";24import { EmptyState, ErrorState, OfflineNotice } from "@/components/ui/states";25import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";26import { SourceHistory } from "@/components/ui/timeline";27import { api } from "@/lib/api";28import type { DetailFacilitiesMeta } from "@/lib/dto";29import { countryName, fmtDate, fmtInt, fmtMw, fmtPct, pluralize } from "@/lib/format";30import { OPERATOR_KIND_LABEL } from "@/lib/labels";31import { routes } from "@/lib/routes";32import { compactJsonLd, NOT_FOUND_METADATA, pageMetadata } from "@/lib/seo";33import type { SearchParams } from "@/lib/sort";34import { CloudStatusBadge } from "../../cloud-regions/cloud-format";3536export const revalidate = 300;3738type Props = { params: Promise<{ slug: string }>; searchParams: Promise<SearchParams> };3940function describe(o: { name: string; kind: string | null; facilityCount: number; countryCount: number; knownMw?: number | null; plannedMw?: number | null; hqCountryIso2?: string | null }): string {41 const parts = [`${o.name} — ${o.kind ? OPERATOR_KIND_LABEL[o.kind as keyof typeof OPERATOR_KIND_LABEL].toLowerCase() : "data center operator"}`];42 parts.push(`${fmtInt(o.facilityCount)} ${pluralize(o.facilityCount, "facility", "facilities")} in ${fmtInt(o.countryCount)} ${pluralize(o.countryCount, "country", "countries")}`);43 if (o.knownMw) parts.push(`${fmtMw(o.knownMw)} known operational capacity`);44 if (o.plannedMw) parts.push(`≈ ${fmtMw(o.plannedMw)} in the pipeline`);45 if (o.hqCountryIso2) parts.push(`headquartered in ${countryName(o.hqCountryIso2)}`);46 return `${parts.join(", ")}. Footprint map, pipeline, expansion velocity, geographic diversity, corporate events, claims and data quality on DataCenterIndex.`;47}4849export async function generateMetadata({ params }: Props): Promise<Metadata> {50 const { slug } = await params;51 const r = await api.operator(slug);52 if (!r.ok) return r.status === 404 ? NOT_FOUND_METADATA : { title: "Operator", alternates: { canonical: routes.operator(slug) } };53 return pageMetadata({ title: r.data.name, description: describe(r.data), path: routes.operator(slug) });54}5556/** Corporate events are served separately by the API; everything else on the operator is a physical change. */57const CORPORATE_TYPES = new Set<EventType>(["acquisition", "investment_announced", "partnership", "executive_change", "customer_agreement", "owner_changed", "operator_changed", "power_agreement"]);5859export default async function OperatorPage({ params, searchParams }: Props) {60 const [{ slug }, sp] = await Promise.all([params, searchParams]);61 const fPage = parseFPage(sp.fPage);62 const r = await api.operator(slug, fPage > 1 ? { fPage } : undefined);63 if (!r.ok && r.status === 404) notFound();64 if (!r.ok) {65 return (66 <Page className="py-6">67 <OfflineNotice detail={r.error} className="mb-4" />68 <ErrorState />69 </Page>70 );71 }72 const o = r.data;73 const meta = (r.meta ?? {}) as DetailFacilitiesMeta;74 const facTotal = meta.facilitiesTotal ?? o.facilityCount;75 const facPer = meta.facilitiesPerPage ?? 50;76 const base = routes.operator(slug);77 const kindLabel = o.kind ? OPERATOR_KIND_LABEL[o.kind] : null;78 const aliases = o.aliases.filter((a) => a && a !== o.name);79 const sameAs = Object.values(o.externalIds ?? {})80 .map(String)81 .filter((v) => /^https?:\/\//.test(v));82 const statusTotal = Object.values(o.statusBreakdown).reduce((a, b) => a + (b ?? 0), 0);83 const corporateIds = new Set(o.corporateEvents.map((e) => e.id));84 // physical = every non-corporate event (generic types such as news / page_changed stay on this side rather than being dropped)85 const physical: EventDTO[] = o.recentEvents.filter((e) => !corporateIds.has(e.id) && !CORPORATE_TYPES.has(e.eventType));86 const corporate: EventDTO[] = o.corporateEvents.length ? o.corporateEvents : o.recentEvents.filter((e) => CORPORATE_TYPES.has(e.eventType));87 const constructionMw = o.constructionMw ?? o.pipeline.construction.mw;88 const plannedMw = o.plannedMw ?? (o.pipeline.approved.mw === null && o.pipeline.announced.mw === null ? null : (o.pipeline.approved.mw ?? 0) + (o.pipeline.announced.mw ?? 0));89 const coverage = o.mwCoverage ?? o.pipeline.mwCoverage;90 const cloud = [...o.cloudRegions].sort((a, b) => a.code.localeCompare(b.code));9192 const jsonLd = compactJsonLd({93 "@context": "https://schema.org",94 "@type": "Organization",95 "@id": `${routes.operator(slug)}#org`,96 name: o.name,97 url: o.website ?? undefined,98 alternateName: aliases.length ? aliases : undefined,99 sameAs: sameAs.length ? sameAs : undefined,100 description: o.description ?? describe(o),101 parentOrganization: o.parent ? { "@type": "Organization", name: o.parent.name } : undefined,102 location: o.hqCountryIso2 ? { "@type": "Country", name: countryName(o.hqCountryIso2) } : undefined,103 });104105 const sections = [106 { id: "map", label: "Map", count: o.facilityCount },107 { id: "pipeline", label: "Pipeline" },108 { id: "geography", label: "Geography", count: o.topCountries.length + o.topMetros.length },109 { id: "velocity", label: "Velocity" },110 { id: "events", label: "Events", count: physical.length + corporate.length },111 { id: "ai", label: "AI facilities", count: o.aiFacilities.length },112 { id: "cloud", label: "Cloud regions", count: cloud.length },113 { id: "projects", label: "Projects", count: o.projects.length },114 { id: "facilities", label: "Facilities", count: facTotal },115 { id: "claims", label: "Claims", count: o.claims.length },116 { id: "quality", label: "Data quality" },117 { id: "history", label: "Source history", count: o.sourceHistory.length },118 ];119120 return (121 <Page>122 <JsonLd data={jsonLd} />123 <div className="pt-3">124 <Breadcrumbs items={[{ name: "Operators", href: routes.operators() }, { name: o.name, href: base }]} />125 </div>126127 <header className="py-5 sm:py-7">128 <div className="flex flex-wrap items-center gap-2">129 <div className="label-caps">Operator</div>130 {kindLabel && (131 <Badge tone={o.kind === "hyperscaler" ? "accent" : "outline"} size="xs">132 {kindLabel}133 </Badge>134 )}135 {o.isCloudProvider && (136 <Badge tone="outline" size="xs">137 Cloud provider138 </Badge>139 )}140 {o.isCarrier && (141 <Badge tone="outline" size="xs">142 Carrier143 </Badge>144 )}145 </div>146 <div className="mt-1.5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">147 <div className="min-w-0">148 <h1 className="text-[24px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">{o.name}</h1>149 <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[13px] text-ink-2">150 {o.hqCountryIso2 && (151 <span className="inline-flex items-center gap-1.5">152 <span className="text-ink-3">HQ</span>153 <Link href={routes.country(o.hqCountryIso2.toLowerCase())} className="inline-flex items-center gap-1.5 hover:text-ink hover:underline">154 <CountryFlag iso2={o.hqCountryIso2} withName />155 </Link>156 </span>157 )}158 {o.website && <ExtLink href={o.website} className="no-underline hover:underline" />}159 {o.parent && (160 <span>161 <span className="text-ink-3">Part of </span>162 <Link href={routes.operator(o.parent.slug)} className="text-ink hover:text-accent hover:underline">163 {o.parent.name}164 </Link>165 </span>166 )}167 </div>168 </div>169 <div className="flex shrink-0 flex-wrap items-center gap-2">170 <WatchButton entityType="operator" slug={o.slug} entityId={o.id} name={o.name} />171 <PillLink href={routes.compareOperators([o.slug])}>Compare with…</PillLink>172 </div>173 </div>174 {o.description && <p className="mt-3 max-w-2xl text-[13.5px] text-ink-2">{o.description}</p>}175 {aliases.length > 0 && (176 <div className="mt-3 flex flex-wrap items-center gap-1.5">177 <span className="text-[11.5px] text-ink-3">Also known as</span>178 {aliases.slice(0, 6).map((a) => (179 <Badge key={a} tone="outline" size="xs" className="max-w-[200px]">180 <span className="truncate">{a}</span>181 </Badge>182 ))}183 {aliases.length > 6 && <span className="text-[11.5px] text-ink-4">+{aliases.length - 6}</span>}184 </div>185 )}186 </header>187188 <MetricRow cols={5}>189 <MetricCell>190 <Metric value={fmtInt(o.facilityCount)} label="Facilities" note={statusTotal ? `${fmtInt(o.pipeline.operational.count)} operational` : undefined} />191 </MetricCell>192 <MetricCell>193 <Metric value={fmtInt(o.countryCount)} label="Countries" />194 </MetricCell>195 <MetricCell>196 <Metric value={o.metroCount === undefined ? "—" : fmtInt(o.metroCount)} label="Markets" />197 </MetricCell>198 <MetricCell>199 <Metric value={<MwFigure mw={o.knownMw} unitClassName="text-[0.5em]" />} label="Known operational MW" note={o.knownMw ? `published figures · ${fmtPct(coverage)} coverage` : `no published figure · ${fmtPct(coverage)} of facilities publish MW`} />200 </MetricCell>201 <MetricCell>202 <Metric value={<MwFigure mw={constructionMw} estimate unitClassName="text-[0.5em]" />} label="Under construction" note={constructionMw === null ? `${fmtInt(o.pipeline.construction.count)} ${pluralize(o.pipeline.construction.count, "site")}, MW not published` : `${fmtInt(o.pipeline.construction.count)} ${pluralize(o.pipeline.construction.count, "site")}`} />203 </MetricCell>204 </MetricRow>205 <MetricRow cols={5} className="border-t-0 [&>div]:border-t-0">206 <MetricCell>207 <Metric size="sm" value={<MwFigure mw={plannedMw} estimate unitClassName="text-[0.5em]" />} label="Planned MW" note={plannedMw === null ? "not published" : "approved + announced"} />208 </MetricCell>209 <MetricCell>210 <Metric size="sm" value={fmtInt(o.aiCount ?? o.aiFacilities.length)} label="AI sites" />211 </MetricCell>212 <MetricCell>213 <Metric size="sm" value={fmtInt(o.cloudRegionCount ?? cloud.length)} label="Cloud regions" note={o.cloudRegionCount || cloud.length ? "cloud exposure" : "none published"} />214 </MetricCell>215 <MetricCell>216 <Metric size="sm" value={fmtInt(o.projectCount ?? o.projects.length)} label="Projects" note={o.projectPlannedMw ? `≈ ${fmtMw(o.projectPlannedMw)} planned` : undefined} />217 </MetricCell>218 <MetricCell>219 <Metric size="sm" value={<CoverageBar value={coverage} width="w-full" label="Capacity coverage" />} label="Capacity coverage" note="share of facilities with a MW figure" />220 </MetricCell>221 </MetricRow>222223 <SectionNav items={sections} className="mt-4" />224225 <div className="flex flex-col gap-8 pt-6">226 <Section id="map" title="Portfolio map" description="Facilities operated; hollow rings are city-level locations." className="scroll-mt-28">227 <EntityMap filters={{ operator: o.slug }} center={{ lat: 25, lng: 10 }} zoom={1.2} openLabel="Open footprint in map" className="h-[240px] sm:h-[360px]" />228 {statusTotal > 0 && (229 <div className="mt-3">230 <div className="label-caps mb-1.5">Status</div>231 <StackedBar data={o.statusBreakdown} height={10} />232 </div>233 )}234 </Section>235236 <Section id="pipeline" title="Pipeline" description="Facilities by lifecycle bucket with known MW — containment-aware (a campus and its buildings are never both counted)." className="scroll-mt-28">237 <PipelineBars pipeline={o.pipeline} />238 <PublishedOnlyNote className="mt-2" />239 </Section>240241 <Section id="geography" title="Geographic diversity" description="Share of the operator's facilities by country and by market." className="scroll-mt-28">242 <div className="grid gap-6 md:grid-cols-2">243 <div>244 <div className="label-caps mb-2">Top countries</div>245 {o.topCountries.length ? <ShareList items={o.topCountries.map((c) => ({ key: c.iso2, label: c.name, href: routes.country(c.slug), count: c.facilityCount, share: c.share, mw: c.knownMw, iso2: c.iso2 }))} /> : <HonestEmpty title="No facility located in a country yet" />}246 {o.countries.length > o.topCountries.length && <p className="mt-2 text-[11.5px] text-ink-4">Top {o.topCountries.length} of {o.countries.length} countries — the facility table covers all of them.</p>}247 </div>248 <div>249 <div className="label-caps mb-2">Top markets</div>250 {o.topMetros.length ? <ShareList items={o.topMetros.map((m) => ({ key: m.id, label: m.name, href: routes.market(m.slug), count: m.facilityCount, share: m.share, mw: m.knownMw, iso2: m.countryIso2 }))} /> : <HonestEmpty title="No facility assigned to a market yet" detail="facilities outside any defined metro are not counted here" />}251 {o.metros.length > o.topMetros.length && <p className="mt-2 text-[11.5px] text-ink-4">Top {o.topMetros.length} of {o.metros.length} markets.</p>}252 </div>253 </div>254 </Section>255256 <Section id="velocity" title="Expansion velocity" description="New facilities, projects, countries and markets per window; the basis of every date is labelled." className="scroll-mt-28">257 <ExpansionVelocityBlock velocity={o.velocity} />258 </Section>259260 <Section id="events" title="Recent events" description="Physical changes (facilities, projects, openings, capacity) are kept apart from corporate events (acquisitions, financing, partnerships, executive changes)." className="scroll-mt-28" aside={<Link href={routes.live(`operator=${o.slug}`)} className="text-[12px] text-ink-3 hover:text-ink">Live feed →</Link>}>261 <div className="grid gap-6 lg:grid-cols-2">262 <div>263 <h3 className="mb-1.5 text-[13px] font-semibold text-ink">264 Physical <span className="figure text-[11px] font-normal text-ink-3">{fmtInt(physical.length)}</span>265 </h3>266 <EventList events={physical.slice(0, 12)} dense emptyTitle="No physical change detected yet" emptyDescription="Facility, project, opening and capacity changes appear here as connectors observe them." />267 </div>268 <div>269 <h3 className="mb-1.5 text-[13px] font-semibold text-ink">270 Corporate <span className="figure text-[11px] font-normal text-ink-3">{fmtInt(corporate.length)}</span>271 </h3>272 <EventList events={corporate.slice(0, 12)} dense emptyTitle="No corporate event recorded" emptyDescription="Acquisitions, financing, partnerships and executive changes are attached to the operator, never turned into projects." />273 </div>274 </div>275 </Section>276277 <Section id="ai" title="AI facilities" description="Sites with confirmed or likely AI / HPC evidence; a single keyword never confirms a site." className="scroll-mt-28" aside={<Link href={routes.ai()} className="text-[12px] text-ink-3 hover:text-ink">AI index →</Link>}>278 {o.aiFacilities.length ? <FacilityTable items={o.aiFacilities} columns={["market", "country", "status", "mw", "type"]} /> : <HonestEmpty title="No AI / HPC facility with evidence" detail="facilities appear here when a source describes them as AI, HPC, GPU or accelerated-computing infrastructure" />}279 </Section>280281 <Section id="cloud" title="Cloud regions" description={o.isCloudProvider ? "Public cloud regions operated by this provider." : "Cloud regions associated with this operator's markets or tenants."} className="scroll-mt-28">282 {cloud.length ? (283 <Table dense mobileCollapse={{ total: cloud.length, label: "cloud regions" }}>284 <THead>285 <tr>286 <Th>Code</Th>287 <Th className="hidden sm:table-cell">Region</Th>288 <Th>Country</Th>289 <Th numeric className="hidden md:table-cell">290 AZs291 </Th>292 <Th>Status</Th>293 <Th className="hidden lg:table-cell">Launched</Th>294 </tr>295 </THead>296 <TBody>297 {cloud.map((cr) => (298 <Tr key={cr.id}>299 <Td primary>300 <RowLink href={routes.cloudRegion(cr.slug)} className="figure text-[12.5px]">301 {cr.code}302 </RowLink>303 <div className="text-[11.5px] text-ink-3 sm:hidden">{cr.name}</div>304 </Td>305 <Td label="Region" className="hidden text-ink-2 sm:table-cell">306 {cr.name}307 </Td>308 <Td label="Country">309 <CountryFlag iso2={cr.countryIso2} withName className="max-w-[160px] text-ink-2" />310 </Td>311 <Td label="AZs" numeric className="hidden md:table-cell">312 {cr.availabilityZones ?? <span className="text-ink-4">—</span>}313 </Td>314 <Td label="Status">315 <CloudStatusBadge status={cr.status} />316 </Td>317 <Td label="Launched" className="figure hidden text-[12px] text-ink-3 lg:table-cell">318 {fmtDate(cr.launchedOn)}319 </Td>320 </Tr>321 ))}322 </TBody>323 </Table>324 ) : (325 <HonestEmpty title="No cloud region linked" detail="cloud exposure is published by the providers' region lists" />326 )}327 </Section>328329 <Section id="projects" title="Projects" description="Announced, permitted and under-construction developments linked to this operator (announcements, not facilities)." className="scroll-mt-28" aside={o.projects.length > 0 ? <Link href={routes.projects(`operator=${o.slug}`)} className="text-[12px] text-ink-3 hover:text-ink">All projects →</Link> : undefined}>330 <ProjectList items={o.projects} emptyTitle="No project linked to this operator" emptyDescription="Projects are created only from announcements that name a site, a location and a development verb." />331 </Section>332333 <Section id="facilities" title="Facilities" description={`${fmtInt(facTotal)} indexed · sorted by known capacity`} className="scroll-mt-28">334 <PagedFacilities items={o.facilities} total={facTotal} page={fPage} perPage={facPer} hrefFor={(p) => fPageHref(base, sp, p)} columns={["market", "country", "status", "mw", "type", "confidence"]} />335 </Section>336337 <Section id="claims" title="Claims" description="Every capacity and investment figure asserted about this operator, with its scope. Portfolio and company figures are shown as evidence, never summed into facilities." className="scroll-mt-28">338 <ClaimsTable claims={o.claims} kind="all" emptyNote="No operator-level claim has been extracted yet — portfolio and company figures appear here once a document asserts one." />339 </Section>340341 <Section id="quality" title="Data quality" description="Sources, completeness, claims by status and open sanity flags for this record." className="scroll-mt-28">342 <DataQualityPanel q={o.dataQuality} />343 </Section>344345 <Section id="history" title="Source history" description="Observations and page changes that shaped this record." className="scroll-mt-28">346 {o.sourceHistory.length ? <SourceHistory items={o.sourceHistory.slice(0, 40)} /> : <EmptyState compact title="No source history yet" />}347 </Section>348 </div>349350 <SourcesFooter sources={r.sources} updatedAt={o.updatedAt} note={o.knownMw === null ? `No operational MW figure has been published by the sources indexed for ${o.name}; counts are exact, capacities appear only when a source publishes them.` : undefined} />351 </Page>352 );353}354